/*
 * Copyright 2011 Clean GWT
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package com.cleangwt.page.rebind;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.cleangwt.page.client.AsyncPage;
import com.cleangwt.page.client.IsPage;
import com.cleangwt.page.client.PageContext;
import com.cleangwt.page.client.PageContext.CodeSplit;
import com.cleangwt.page.client.PageContext.Page;
import com.cleangwt.page.client.PageContext.Prefetch;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.RunAsyncCallback;
import com.google.gwt.core.client.prefetch.Prefetcher;
import com.google.gwt.core.client.prefetch.RunAsyncCode;
import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

/**
 * A generator for creating {@link PageContext}.
 * 
 * @author Jess
 * @version 1.0
 * @since 1.0
 */
public class PageContextGenerator extends Generator {
  private boolean codePrefetching;

  @Override
  public String generate(TreeLogger logger, GeneratorContext context, String typeName)
      throws UnableToCompleteException {
    TypeOracle typeOracle = context.getTypeOracle();
    JClassType classType = typeOracle.findType(typeName);
    String packageName = classType.getPackage().getName();
    String className = classType.getSimpleSourceName() + "Impl";
    PrintWriter printWriter = context.tryCreate(logger, packageName, className);
    // print writer if null, source code has ALREADY been generated
    if (printWriter == null) {
      return packageName + "." + className;
    }
    // init composer, set class properties, create source writer
    ClassSourceFileComposerFactory composer = null;
    composer = new ClassSourceFileComposerFactory(packageName, className);
    composer.setSuperclass(typeName);
    composer.addImport(GWT.class.getCanonicalName());
    composer.addImport(AsyncPage.class.getCanonicalName());
    composer.addImport(IsPage.class.getCanonicalName());
    composer.addImport(RunAsyncCallback.class.getCanonicalName());
    composer.addImport(AsyncCallback.class.getCanonicalName());
    composer.addImport(RunAsyncCode.class.getCanonicalName());
    composer.addImport(Map.class.getCanonicalName());
    composer.addImport(HashMap.class.getCanonicalName());
    composer.addImport(List.class.getCanonicalName());
    composer.addImport(ArrayList.class.getCanonicalName());
    composer.addImport(Prefetcher.class.getCanonicalName());
    SourceWriter sw = composer.createSourceWriter(context, printWriter);

    createInitializer(sw, className, typeOracle.findType(IsPage.class.getName()).getSubtypes());
    createFields(sw);
    sw.outdent();
    sw.println("}");

    context.commit(logger, printWriter);
    return packageName + "." + className;
  }

  private void createFields(SourceWriter sw) {
    if (codePrefetching) {
      sw.println("private Map<String, List<RunAsyncCode>> prefetchingGroups = new HashMap<String, List<RunAsyncCode>>();");
    }
  }

  private void createInitializer(SourceWriter sw, String className, JClassType[] types)
      throws UnableToCompleteException {
    List<String> boundPages = new ArrayList<String>();
    List<String> boundGroups = new ArrayList<String>();
    sw.println(className + "(){");
    for (JClassType type : types) {
      if (!type.isAnnotationPresent(Page.class)) {
        continue;
      }
      CodeSplit codeSplit = type.getAnnotation(CodeSplit.class);
      Prefetch prefetch = null;
      String prefetchingGroup = null;

      String pageName = type.getAnnotation(Page.class).value();

      if (pageName.isEmpty()) {
        // default by simple source name
        pageName = type.getSimpleSourceName();
      }

      // prefetching dependents on code spliting
      if (codeSplit != null) {
        prefetch = type.getAnnotation(Prefetch.class);

        if (prefetch != null) {
          codePrefetching = true;
          prefetchingGroup = prefetch.value();
        }
      }

      if (boundPages.contains(pageName)) {
        throw new IllegalArgumentException("Duplicate page name '" + pageName + "'");
      } else {
        boundPages.add(pageName);
      }
      // initialize asyncPages
      sw.indent();
      sw.println("asyncPages.put(\"%s\",new AsyncPage(){", pageName);
      sw.indent();
      sw.println("public void runAsync(final AsyncCallback<IsPage> callback){");
      sw.indent();
      if (codeSplit != null) {
        sw.println("GWT.runAsync(%s.class, new RunAsyncCallback() {", type.getQualifiedSourceName());
        sw.indent();
        sw.println("public void onFailure(Throwable caught) {");
        sw.indent();
        sw.println(" callback.onFailure(caught);");
        sw.outdent();
        sw.println("}");

        sw.println("public void onSuccess() {");
        sw.indent();
        if (type.isAbstract()) {
          // try re-binding
          sw.println("callback.onSuccess((IsPage)GWT.create(%s.class));", type
              .getQualifiedSourceName());
        } else {
          sw.println("callback.onSuccess(new %s());", type.getQualifiedSourceName());
        }
        if (prefetch != null) {
          // prefetch page group after page loaded
          sw.println("if (prefetchingGroups.containsKey(\"%s\")) {", prefetchingGroup);
          sw.indent();
          sw.println("Prefetcher.prefetch(prefetchingGroups.remove(\"%s\"));", prefetchingGroup);
          sw.outdent();
          sw.println("}");
        }
        sw.outdent();
        sw.println("}});");
      } else {
        if (type.isAbstract()) {
          // try re-binding
          sw.println("callback.onSuccess((IsPage)GWT.create(%s.class));", type
              .getQualifiedSourceName());
        } else {
          sw.println("callback.onSuccess(new %s());", type.getQualifiedSourceName());
        }
      }
      sw.outdent();
      sw.println("}});");
      if (prefetch != null) {
        // mapping page group to split points
        if (!boundGroups.contains(prefetchingGroup)) {
          boundGroups.add(prefetchingGroup);
          sw.println("prefetchingGroups.put(\"%s\",new ArrayList<RunAsyncCode>());",
              prefetchingGroup);
        }
        sw.println("prefetchingGroups.get(\"%s\").add(RunAsyncCode.runAsyncCode(%s.class));",
            prefetchingGroup, type.getQualifiedSourceName());
      }
    }

    if (codePrefetching) {
      sw.println("Prefetcher.start();");
    }
    sw.outdent();
    sw.println("}");

  }

}
